<?php
/**
 * Socket Server
 *
 * @author Philippe Gerber <philippe@bandground.net>
 * @package Randstand_Net_Socket
 */
class Randstand_Net_Socket_Server
{
    /**
     * Host to connect to
     *
     * @var string
     */
    private $_host = null;
    
    /**
     * Port to connect on
     *
     * @var int
     */
    private $_port = -1;
    
    /**
     * Master socket
     *
     * @var Resource
     */
    private $_socketHandle = null;
    
    /**
     * clients 
     */
    private $_clients = null;
    
    /**
     * Instance holder
     *
     * @var Randstand_Net_Socket_Server
     */
    private static $_instance = null;
    
    
    public static function getInstance()
    {
        if (null === self::$_instance) {
            self::$_instance = new self();
        }
        
        return self::$_instance;
    }
    
    
    
    /**
     * Constructor
     *
     * @return Randstand_Net_Socket
     */
    function __construct()
    {
        $this->_clients = array();
        return $this;
    }
    
    
    /**
     * Set the port to listen on
     *
     * @param int $argPort
     */
    public function setPort($argPort)
    {
        $this->_port = $argPort;
        return $this;
    }
    
    
    /**
     * Set the host to listen on
     *
     * @param string $argHost
     */
    public function setHost($argHost)
    {
        $this->_host = $argHost;
        return $this;
    }
    
    
    /**
     * Create a new socket
     *
     * @param int $argDomain
     * @param int $argType
     * @param int $argProtocol
     * @return Randstand_Net_Socket
     */
    public function create($argDomain = AF_INET, $argType = SOCK_STREAM, $argProtocol = SOL_TCP)
    {
    	$this->_socketHandle = socket_create($argDomain, $argType, $argProtocol);
    	return $this;
    }
    
    
    /**
     * Set socket options
     *
     * @param int $argLevel
     * @param string $argOptName
     * @param unknown_type $argOptVal
     * @return Randstand_Net_Socket
     */
    public function setOption($argLevel, $argOptName = null, $argOptVal = -1)
    {
    	socket_set_option($this->_socketHandle, $argLevel, $argOptName, $argOptVal);
    	return $this;
    }
    
    
    /**
     * If true, the socket_accept will block
     * till a client connects
     *
     * @param bool $argBlock
     * @return Randstand_Net_Socket
     */
    public function setBlocking($argBlock)
    {
        if ($argBlock) {
    	    socket_set_block($this->_socketHandle);
        }
        else {
            socket_set_nonblock($this->_socketHandle);
        }
        return $this;
    }
    
    
    
    /**
     * Listen
     *
     * @return Randstand_Net_Socket
     */
    public function listen($argBackLog = 5)
    {
    	socket_listen($this->_socketHandle, $argBackLog);
    	return $this;
    }
    
    
    
    /**
     * Bind the socket
     *
     * @return Randstand_Net_Socket
     */
    public function bind()
    {
        socket_bind($this->_socketHandle, $this->_host, $this->_port);
        return $this;
    }
    
    
    /**
     * Enter description here...
     *
     * @return Randstand_Net_Socket
     */
    public function checkForNewClient()
    {
        $possibleClient = @socket_accept($this->_socketHandle);
        if ($possibleClient) {
            $client = new Randstand_Net_Socket_Server_Client($possibleClient);
            $this->_clients[] = $client;
            
            //send welcome message
            $msg = Randstand_Net_Socket_Server_Message::getInstance()
                       ->getFormattedMessage('welcome', $client->getToken());
            $client->sendMessage($msg);
            
            //log
            echo "{$client->getToken()} >> $msg";
        }
        return $this;
    }
    
    
    /**
     * Enter description here...
     *
     * @param int $argTimeout
     * @return Randstand_Net_Socket
     */
    public function waitForDataFromClients($argTimeout)
    {
        @socket_select($this->_getAllClientHandles(), $write = null, $except = null, $argTimeout);
        return $this;
    }
    
    
    /**
     * Enter description here...
     *
     * @return Randstand_Net_Socket
     */
    public function readDataFromClients()
    {
        //loop over all the clients
        foreach ($this->_clients as $client) {
            //try to read something
            $buffer = @socket_read($client->getHandle(), 2048, PHP_BINARY_READ);
            
            //if there's content ...
        	if (strlen($buffer) > 0) {
        	    
        	    //if content is a linebreak ...
        	    if ($buffer == Randstand_Net_Socket_Server_Message::LINE_BREAK) {
        	        
        	        //validate client request
        	        if ($this->_validateRequest($client->getRequest())) {
        	            
        	            //process the request ...
        	            try {
        	                Randstand_Net_Socket_Server_Command_Manager::load(
        	                    $this->_getCommandByRequest($client->getRequest()),
        	                    $this->_getArgumentByRequest($client->getRequest()),
        	                    $client
        	                );
                    	    
                    	    echo "{$client->getToken()} << {$client->getRequest()}\n";
        	            }
        	            catch (Randstand_Net_Socket_Server_Command_Exception $e)
        	            {}
        	        }
        	        else {
        	            $msg = Randstand_Net_Socket_Server_Message::getInstance()
                                   ->getFormattedMessage('invalid_request');
        	            $client->sendMessage($msg);
        	        }
        	        
        	        $client->clearRequest();
        	    }
        	    
        	    //... else append the data 
        	    else {
        	        $client->appendRequest($buffer);
        	    }
        	}
        }
        return $this;
    }
    
    
    /**
     * Enter description here...
     *
     * @param string $argRqst
     * @return bool
     */
    private function _validateRequest($argRqst)
    {
        $command = $this->_getCommandByRequest($argRqst);
        if (!$command) {
            return false;
        }
        
        if (!Randstand_Net_Socket_Server_Command_Manager::hasCommand($command)) {
            return false;
        }
        
        return true;
    }
    
    
    private function _getCommandByRequest($argRqst)
    {
        if (empty($argRqst)) {
    	    return false;
    	}
    	
    	if (substr($argRqst, 0, 1) != '/') {
    	    return false;
    	}
    	
    	$chunks  = explode(' ', substr($argRqst, 1));
        $command = (!count($chunks)) ? substr($argRqst, 1) : $chunks[0];
        
        if (empty($command)) {
            return false;
        }
        
        return $command;
    }
    
    
    private function _getArgumentByRequest($argRqst)
    {
        $chunks = explode(' ', substr($argRqst, 1));
        
        if (!count($chunks)) {
            return false;
        }
        
        array_shift($chunks);
        return implode(' ', $chunks);
    }
    
    
    public function getClients()
    {
        return $this->_clients;
    }
    
    
    /**
     * Return all client handles
     *
     * @return array
     */
    private function _getAllClientHandles()
    {
    	$aRet = array();
    	foreach ($this->_clients as $client) {
    	    $aRet[] = $client->getHandle();
    	}
    	return $aRet;
    }
    
}
?>
